package com.example.shop.es.service.impl;

import com.example.shop.es.dto.ElasticSearchQueryParam;
import com.example.shop.es.service.ElasticSearchClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.UncategorizedElasticsearchException;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;

import java.lang.reflect.Field;
import java.util.*;

@Service
public class ElasticSearchClientImpl implements ElasticSearchClient {

    private final ObjectMapper objectMapper;
    private final ElasticsearchRestTemplate esTemplate;

    public ElasticSearchClientImpl(@Qualifier("esObjectMapper") ObjectMapper objectMapper, ElasticsearchRestTemplate esTemplate) {
        this.objectMapper = objectMapper;
        this.esTemplate = esTemplate;
    }


    /**
     * 获取类上的索引名
     * @param tClass  @Document注解标记的类
     * @return 返回索引名
     */
    private String getIndexName(Class<?> tClass){
        Document documentAnnotation = tClass.getDeclaredAnnotation(Document.class);
        return documentAnnotation.indexName();
    }

    /**
     * 获取data上的文档id
     * @param data 文档数据
     * @param <T> 文档类型
     * @return 返回文档id
     */
    private <T> String getId(T data){
        try {
            Class<?> aClass = data.getClass();
            do{
                //设置该文档的id为该类上@id标记的该字段的值
                Field[] fields = aClass.getDeclaredFields();
                for(Field field:fields){
                    field.setAccessible(true);
                    //由于可能有继承关系，所以必须要可以拿到父类的属性
                    if(field.getAnnotation(Id.class)!=null){
                        return String.valueOf(field.get(data));
                    }
                }
                aClass = aClass.getSuperclass();
                //当为根时，即Object类时，该类加载器为null，因为它是由C++本地代码写的
            }while (aClass.getClassLoader()!=null);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 删除document所有value值为空的k-v
     * @param document 要更新的map文档数据
     * @param k 当前的key
     * @param v 当前的value
     */
    private void removeValueNull(Iterator<? extends Map.Entry<?, ?>> document, Object k, Object v){
        if(v == null){
            document.remove();
        }else if(v instanceof LinkedHashMap){
            Iterator<? extends Map.Entry<?, ?>> iterator = ((Map<?, ?>) v).entrySet().iterator();
            while (iterator.hasNext()){
                Map.Entry<?, ?> next = iterator.next();
                removeValueNull(iterator,next.getKey(),next.getValue());
            }
        }
    }


    private <T> List<T> getResult(List<SearchHit<T>> searchHitList, Class<T> tClass){
        ArrayList<T> tList = new ArrayList<>();
        getResult(tList,searchHitList,tClass);
        return tList;
    }
    private <T> void getResult(List<T> result,List<SearchHit<T>> searchHitList, Class<T> tClass){
        for (SearchHit<T> hit:searchHitList){
            T content = hit.getContent();
            Map<String, List<String>> highlightFields = hit.getHighlightFields();
            if(highlightFields!=null){
                highlightFields.forEach((k,v)->{
                    try {
                        Field field = tClass.getDeclaredField(k.split("\\.")[0]);
                        field.setAccessible(true);
                        field.set(content,v.get(0));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
            result.add(content);
        }
    }

    /**
     * 获取UpdateQuery.Builder。
     * @param obj 要更新的数据，如果进行脚本更新，则该对象必须指定id，因为该id将会作为文档的id
     * @param script 条件更新的脚本，符合条件才更新。如果为null，则只要有一个条件，那么就是id
     * @return 返回UpdateQuery.Builder
     */
    private UpdateQuery.Builder UpdateQueryBuilder(Object obj,String script, Map<String, Object> params){
        try{
            UpdateQuery.Builder builder = UpdateQuery.builder(getId(obj));
            //需要复杂更新可以设置脚本
            if(script!=null){
                //脚本参数
                if(params!=null){
                    builder.withParams(params);
                }
                builder.withScript(script)
                        .withLang("painless")
                        //进行脚本方式的更新
                        .withScriptedUpsert(true);
            }else{
                //生成data对应的文档数据
                org.springframework.data.elasticsearch.core.document.Document document = org.springframework.data.elasticsearch.core.document.Document.parse(objectMapper.writeValueAsString(obj));
                //将文档数据中值为null的字段
                Iterator<Map.Entry<String, Object>> iterator = document.entrySet().iterator();
                while (iterator.hasNext()){
                    Map.Entry<String, Object> next = iterator.next();
                    removeValueNull(iterator,next.getKey(),next.getValue());
                }

                //设置要更新的Document文档数据
                builder.withDocument(document)
                        //进行doc方式的文档更新，如果更新的字段不存在，则会添加该字段
                        //如果进行doc方式，则不能使用脚本
                        .withDocAsUpsert(true);
            }
            //更新冲突重试次数
            builder.withRetryOnConflict(5)
                    //立即更新
                    .withRefresh(UpdateQuery.Refresh.True);
            return builder;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据ElasticSearchQueryParam获取query查询条件
     * @param queryParam 查询参数
     * @return 返回Query
     */
    private Query getQuery(ElasticSearchQueryParam queryParam){
        Query query = new NativeSearchQuery(queryParam.getQueryBuilder());
        //返回的字段设置
        if (queryParam.getFields()!=null){
            query.addFields(queryParam.getFields());
        }
        //要过滤的字段设置
        if(queryParam.getSourceFilter()!=null){
            query.addSourceFilter(queryParam.getSourceFilter());
        }
        //分页设置
        if(queryParam.getPageable()!=null){
            query.setPageable(queryParam.getPageable());
        }
        //排序设置
        if(queryParam.getSort()!=null){
            query.addSort(queryParam.getSort());
        }
        //高亮处理设置
        //如果高亮显示不完整，那是因为如果用分词则会高亮分词，如果是keyword，则高亮一整个
        if(queryParam.getHighlightQuery()!=null){
            HighlightBuilder highlight = queryParam.getHighlightQuery().getHighlightBuilder();
            //片段的大小
            highlight.fragmentSize(Integer.MAX_VALUE)
                    //设置为0即可返回完整内容 而非片段
                    .numOfFragments(0)
                    //多个字段高亮，则需要设置这个为false
                    .requireFieldMatch(false);
            query.setHighlightQuery(queryParam.getHighlightQuery());
        }
        return query;
    }

//=========================================  索引(相当于表)  ===========================================//

    @Override
    public boolean createIndex(Class<?> tClass) {
        //获取索引操作
        IndexOperations indexOperations = esTemplate.indexOps(tClass);
        boolean createState = false;
        //如果索引存在，则不操作
        if(!indexOperations.exists()){
            //创建索引
            createState = indexOperations.create();
            //并将索引的结构写入刚刚创建的索引中
            indexOperations.putMapping(indexOperations.createMapping(tClass));
        }
        return createState;
    }

    @Override
    public boolean deleteIndex(Class<?> tClass) {
        //获取索引操作
        IndexOperations indexOperations = esTemplate.indexOps(tClass);
        return indexOperations.delete();
    }

    @Override
    public boolean addIndexAlias(Class<?> tClass, String aliasName) {
        //获取索引操作
        IndexOperations indexOperations = esTemplate.indexOps(tClass);
        return indexOperations.addAlias(new AliasQuery(aliasName));
    }

    @Override
    public boolean existIndex(Class<?> tClass) {
        //获取索引操作
        IndexOperations indexOperations = esTemplate.indexOps(tClass);
        return indexOperations.exists();
    }

//==========================================  文档(相当于记录)  ===========================================//

    @Override
    public  boolean saveDocument(Object data) {
        if(data == null || esTemplate.save(data)!=null){
            return true;
        }
        return false;
    }

    @Override
    public boolean saveDocument(Object data, WriteRequest.RefreshPolicy refreshPolicy) {
        return bulkSave(Arrays.asList(data),refreshPolicy);
    }

    @Override
    public boolean updateDocument(Object data) {
       if(data==null){
           return false;
       }

       return updateDocument(data,null,null);
    }

    @Override
    public boolean updateDocument(Object data, String script, Map<String, Object> params) {
        try {
            if(data==null){
                return false;
            }
            UpdateQuery.Builder builder = UpdateQueryBuilder(data,script,params);
            try{
                UpdateResponse response = esTemplate.update(builder.build(), IndexCoordinates.of(getIndexName(data.getClass())));
                if (response.getResult() == UpdateResponse.Result.UPDATED) {
                    return true;
                }
            }catch (UncategorizedElasticsearchException e){
                e.printStackTrace();
                System.out.println("文档不存在，,则不进行更新，错误没问题");
                return true;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public void deleteDocument(Object deleteDocument){
        esTemplate.delete(deleteDocument);
    }

    @Override
    public void deleteDocument(QueryBuilder queryBuilder,Class<?> tClass,String...indexNames) {
        Query query = new NativeSearchQuery(queryBuilder);
        esTemplate.delete(query,tClass,IndexCoordinates.of(indexNames));
    }

    @Override
    public <T> T getDocument(long id,Class<T> tClass) {
        return esTemplate.get(String.valueOf(id), tClass);
    }

    @Override
    public <T> T getDocumentMustClass(long id, Class<T> tClass) {
        //这种获取类型的方式已经被弃用了，是不建议这样的
        //要么保存是用最底子类保存。
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("_class.keyword", tClass.getName()))
                .must(QueryBuilders.termQuery("_id", id));
        List<T> result = search(ElasticSearchQueryParam.builder(queryBuilder).build(), tClass);
        if(result.isEmpty()){
            return null;
        }
        return result.get(0);
    }


    @Override
    public boolean existDocument(long id, Class<?> tClass) {
        return  esTemplate.exists(String.valueOf(id),tClass);
    }


//===========================================  批处理  ===================================================//

    @Override
    public <T> boolean bulkSave(List<T> dataList, WriteRequest.RefreshPolicy refreshPolicy) {
        if(dataList==null || dataList.isEmpty()){
            return true;
        }
        List<IndexQuery> bulkQueryList = new ArrayList<>();
        for(T obj:dataList){
            IndexQuery indexQuery = new IndexQuery();
            indexQuery.setId(getId(obj));
            indexQuery.setObject(obj);
            bulkQueryList.add(indexQuery);
        }
        BulkOptions.BulkOptionsBuilder builder = BulkOptions.builder();
        //设置批处理的请求完后，es服务器刷新数据的策略
        builder.withRefreshPolicy(refreshPolicy)
                //每个请求只要2秒操作时间，否则会报超时
                .withTimeout(TimeValue.timeValueSeconds(5));
        String indexName = getIndexName(dataList.get(0).getClass());
        List<String> list = esTemplate.bulkIndex(bulkQueryList, builder.build(), IndexCoordinates.of(indexName));
        if(list !=null && !list.isEmpty()){
            return true;
        }
        return false;
    }

    @Override
    public <T> void bulkUpdate(List<T> dataList, WriteRequest.RefreshPolicy refreshPolicy) {
        if(dataList==null || dataList.isEmpty()){
            return;
        }
        List<UpdateQuery> bulkQueryList = new ArrayList<>();
        for(int i=0;i<dataList.size();i++){
            bulkQueryList.add(UpdateQueryBuilder(dataList.get(i),null,null).build());
        }
        BulkOptions.BulkOptionsBuilder builder = BulkOptions.builder();
        //设置批处理的请求完后，es服务器刷新数据的策略
        builder.withRefreshPolicy(refreshPolicy)
                //每个请求只要2秒操作时间，否则会报超时
                .withTimeout(TimeValue.timeValueSeconds(2));
        String indexName = getIndexName(dataList.get(0).getClass());
        esTemplate.bulkUpdate(bulkQueryList,builder.build(),IndexCoordinates.of(indexName));
    }

    @Override
    public <T> List<T> search(ElasticSearchQueryParam queryParam,Class<T> tClass) {
        Query query = getQuery(queryParam);
        List<SearchHit<T>> result = esTemplate.search(query, tClass).getSearchHits();
        return getResult(result,tClass);
    }

    @Override
    public <T> List<T> multiSearch(List<ElasticSearchQueryParam> queryParamList, Class<T> tClass) {
        List<Query> queryList = new ArrayList<>();
        for(ElasticSearchQueryParam esParam:queryParamList){
            queryList.add(getQuery(esParam));
        }
        /**
         * 如果响应为null，一般是因为该索引没有的映射结构没有创建完全，所以建议先存个最大的。
         */
        try{
            //当搜索不存在记录时，会报错
            List<SearchHits<T>> multiResult = esTemplate.multiSearch(queryList, tClass, IndexCoordinates.of(getIndexName(tClass)));
            List<T> searchTotalResult = new ArrayList<>();
            for(SearchHits<T> searchResult:multiResult){
                getResult(searchTotalResult,searchResult.getSearchHits(),tClass);
            }
            return searchTotalResult;
        }catch (Exception e){
            return new ArrayList<>();
        }
    }

    @Override
    public Long count(Class<?> tClass, ElasticSearchQueryParam queryParam) {
        return esTemplate.count(getQuery(queryParam), tClass);
    }

    /**
     try{

     }catch (IOException e){
     e.printStackTrace();
     }
     */
}
